package com.imooc.distributelock.lock;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.UUID;

import java.util.concurrent.*;

@Slf4j
public class RedisLock extends AbstractLock{
    private final StringRedisTemplate stringRedisTemplate;

    private final String lockName;

    private final Long defaultExpireTime = 30000L;//设置过期时间30s

    private final String instanceId = UUID.randomUUID().toString();

    //使用构造函数进行赋值
    public RedisLock(StringRedisTemplate stringRedisTemplate, String lockName) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
    }
    //创建线程池
    private static final ScheduledExecutorService RENEWAL_EXECUTOR = Executors.newScheduledThreadPool(3);
    //声明存储可能正在运行的任务
    private static final ConcurrentHashMap<String, ScheduledFuture<?>> RENEWAL_TASKS = new ConcurrentHashMap<>();

    //方法：创建实例中不同线程的键
    public String getLockKey(){
        return instanceId + ":" + Thread.currentThread().getId();
    }

    //创建续期的键值
    public String getRenewalKey(){
        return lockName + ":" + getLockKey();
    }

    //当用户自己直接调用锁而不设置过期时间时，就使用默认的过期时间30s
    @Override
    public void lock(){
        lock(TimeUnit.MILLISECONDS, defaultExpireTime);
    }

    //方法：使用阻塞的方式获取锁，如果获取不到锁则一直等待到获取锁
    @Override
    public void lock(TimeUnit timeUnit, Long expireTime) {
        //统一过期时间的单位为毫秒
        Long expireTimeMillis = timeUnit.toMillis(expireTime);
        //使用阻塞的方式获取锁
        while(!tryLockInterval(expireTimeMillis, TimeUnit.MILLISECONDS)){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.warn("线程中断退出等待");
                return;
            }
        }
    }

    //方法1：如果用户直接进行调用，则直接采用非阻塞的方式获取锁
    @Override
    public boolean tryLock() {
            return tryLockInterval(defaultExpireTime, TimeUnit.MILLISECONDS);
    }
    //方法1，尝试获取锁，使用带超时的阻塞锁，如果用户没有设置过期时间，则使用默认的过期时间30s
    @Override
    public boolean tryLock(long time,TimeUnit unit) throws InterruptedException {
        return tryLock(time,defaultExpireTime, unit);
    }
    //方法2，尝试获取锁，使用带超时的阻塞锁，如果超过等待时间则直接返回false,若没有超过等待时间则尝试获取锁
    @Override
    public boolean tryLock(long time, long expireTime, TimeUnit unit) throws InterruptedException {
        boolean isLock = false;
        //1、进行统一过期时间的单位
        Long startTime = System.currentTimeMillis();
        Long currentTime = System.currentTimeMillis();
        Long waitTime = unit.toMillis(time);
        //2、当在等待时间内就尝试获取锁，否则重新设置当前时间进行判断
        while (currentTime - startTime < waitTime) {
            if(tryLockInterval(expireTime, TimeUnit.MILLISECONDS)){
                isLock = true;
                break;
            }
            // 检查是否被中断
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
            //重新获取当前时间
            currentTime = System.currentTimeMillis();
            // 每次尝试后稍微等待，避免过度消耗CPU
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw e; // 重新抛出InterruptedException，保持方法签名
            }
        }
        return isLock;
    }

    //方法3：在等待时间内尝试获取锁，使用lua脚本实现加锁，并返回结果
    private boolean tryLockInterval(long expireTime, TimeUnit timeUnit) {
        //将过期时间统一为毫秒
        Long expireTimeInMillis = timeUnit.toMillis(expireTime);
        //获取键值
        String lockKey = getLockKey();
        //使用lua脚本进行加锁
        String luaScript =
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "    redis.call('hset', KEYS[1], ARGV[1], 1); " +
                        "    redis.call('pexpire', KEYS[1], ARGV[2]); " +
                        "    return 1; " +
                        "end; " +
                        "if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then " +
                        "    redis.call('hincrby', KEYS[1], ARGV[1], 1); " +
                        "    redis.call('pexpire', KEYS[1], ARGV[2]); " +
                        "    return 1; " +
                        "else " +
                        "    return 0; " +
                        "end";
        Long result = stringRedisTemplate.execute(
                new DefaultRedisScript<>(luaScript,Long.class),
                Collections.singletonList(lockName),
                lockKey,
                expireTimeInMillis.toString()
        );
        //如果获取到锁，则返回true，并且开启调度任务管理器
        if(result != null && result == 1){
           startRenewalTask(expireTimeInMillis);
           return true;
        }
        return false;
    }

    //方法4：开启调度任务管理器，进行锁的续期，如果没有锁不存在则停止当前的续期任务
    private void startRenewalTask(Long expireTimeInMillis) {
        //先获取续期锁的键值
        String renewalKey = getRenewalKey();
        String lockKey = getLockKey();
        //判断该任务是否已经在任务管理器中
        if(RENEWAL_TASKS.containsKey(renewalKey)){
            return;
        }
        //设置间隔时间
        Long intervalTime = expireTimeInMillis / 3;
        //开始定时任务
        ScheduledFuture<?> future = RENEWAL_EXECUTOR.scheduleAtFixedRate(() -> {
            //使用lua脚本实现锁的续期
            try{
                String luaScript =
                        "if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then " +
                                "    redis.call('pexpire', KEYS[1], ARGV[2]); " +
                                "    return 1; " +
                                "else " +
                                "    return 0; " +
                                "end";
                Long result = stringRedisTemplate.execute(
                        new DefaultRedisScript<>(luaScript,Long.class),
                        Collections.singletonList(lockName),
                        lockKey,
                        expireTimeInMillis.toString());
                //判断续期是否成功,当返回值为0，则说明锁已经不存在，则停止任务管理器，否则继续进行任务管理器
                if(result != null && result == 1){
                    log.info("锁续期成功，锁的键值：{}", renewalKey);
                }
                if(result == null || result == 0){
                    log.info("锁续期失败，锁的键值：{}", renewalKey);
                    stopRenewalTask();
                }
                }catch (Exception e){
                    log.error("锁续期异常，锁的键值：{}", renewalKey, e);
                    stopRenewalTask();
            }
        }, intervalTime, intervalTime, TimeUnit.MILLISECONDS);
        RENEWAL_TASKS.put(renewalKey,future);
    }

    //方法4：封装：停止任务管理器
    private void stopRenewalTask() {
        //获取锁的续期键值
        String renewalKey = getRenewalKey();
        //移除该键并且获取该对应的任务
        ScheduledFuture<?> future = RENEWAL_TASKS.remove(renewalKey);
        if(future != null){
            //取消任务：false:不会中断正在执行的任务，允许当前执行的续期操作完成；true:中断正在执行中的任务，并取消剩余任务
            future.cancel(false);
        }
    }

    //方法5：释放锁
    @Override
    public void unlock() {
        //获取锁的键值
        String lockKey = getLockKey();
        //取消续期任务
        stopRenewalTask();
        //使用lua脚本进行锁的释放
        String luaScript =
                "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then " +
                        "    return 0; " +
                        "end; " +
                        "local count = redis.call('hincrby', KEYS[1], ARGV[1], -1); " +
                        "if (count > 0) then " +
                        "    redis.call('pexpire', KEYS[1], ARGV[2]); " +
                        "    return 1; " +
                        "else " +
                        "    redis.call('del', KEYS[1]); " +
                        "    return 1; " +
                        "end";
        stringRedisTemplate.execute(
                new DefaultRedisScript<>(luaScript,Long.class),
                Collections.singletonList(lockName),
                lockKey,
                defaultExpireTime.toString()
        );
    }

    //方法6：安全关闭线程池并清理任务表
    public static void shutdown() {
        for (ScheduledFuture<?> future : RENEWAL_TASKS.values()) {
            future.cancel(false);
        }
        RENEWAL_TASKS.clear();
        RENEWAL_EXECUTOR.shutdown();
    }
}
